# -*- coding: utf-8 -*-

import json
from typing import Dict, Any

from dtcloud import fields


def monkey_patch(cls):
    """ Return a method decorator to monkey-patch the given class. """

    def decorate(func):
        name = func.__name__
        func.super = getattr(cls, name, None)
        setattr(cls, name, func)
        return func

    return decorate


#
# Implement sparse fields by monkey-patching fields.Field
#

fields.Field.__doc__ += """

        .. _field-sparse:

        .. rubric:: Sparse fields

        Sparse fields have a very small probability of being not null. Therefore
        many such fields can be serialized compactly into a common location, the
        latter being a so-called "serialized" field.

        :param sparse: the name of the field where the value of this field must
            be stored.
"""
fields.Field.sparse = None


@monkey_patch(fields.Field)
def _get_attrs(self, model_class, name):
    attrs = _get_attrs.super(self, model_class, name)
    if attrs.get('sparse'):
        # by default, sparse fields are not stored and not copied
        attrs['store'] = False
        attrs['copy'] = attrs.get('copy', False)
        attrs['compute'] = self._compute_sparse
        if not attrs.get('readonly'):
            attrs['inverse'] = self._inverse_sparse
    return attrs


@monkey_patch(fields.Field)
def _compute_sparse(self, records):
    for record in records:
        values = record[self.sparse]
        record[self.name] = values.get(self.name)
    if self.relational:
        for record in records:
            record[self.name] = record[self.name].exists()


@monkey_patch(fields.Field)
def _inverse_sparse(self, records):
    for record in records:
        values = record[self.sparse]
        value = self.convert_to_read(record[self.name], record, use_display_name=False)
        if value:
            if values.get(self.name) != value:
                values[self.name] = value
                record[self.sparse] = values
        else:
            if self.name in values:
                values.pop(self.name)
                record[self.sparse] = values


class TouchDict(Dict[str, Any]):
    """Makes a dictionary behave like an object, with attribute-style access.
        You can get the ObjectId representation of `id` by calling `oid`
    """

    def __getattr__(self, name: str) -> Any:
        try:
            return self[name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name: str, value: Any) -> None:
        self[name] = value


# Definition and implementation of serialized fields


class Serialized(fields.Field):
    """ Serialized fields provide the storage for sparse fields. """
    type = 'serialized'
    column_type = ('text', 'text')

    base_type = None  # must be set to dict, list or tuple

    prefetch = False  # not prefetched by default

    _default_json_mapping = {
        dict: "{}",
        list: "[]",
        tuple: "[]"
    }

    # def __init__(self, string=fields.Default, base_type=fields.Default, **kwargs):
    #     super().__init__(string=string, base_type=base_type, **kwargs)
    #
    # def _setup_attrs(self, model, name):  # pylint: disable=missing-return
    #     super()._setup_attrs(model, name)
    #     if self.base_type not in self._default_json_mapping:
    #         raise ValueError("%s is not a supported base type" % (self.base_type))

    def convert_to_column(self, value, record, values=None, validate=True):
        return self.convert_to_cache(value, record, validate=validate)

    def convert_to_cache(self, value, record, validate=True):
        # cache format: json.dumps(value) or None
        # return json.dumps(value) if isinstance(value, (dict, list)) else (value or None)
        if isinstance(value, self.base_type):
            return json.dumps(value)
        else:
            return value or None

    def convert_to_record(self, value, record):
        default = self._default_json_mapping.get(self.base_type) or "{}"
        return json.loads(value or default)


fields.Serialized = Serialized
